<?php

/**
 * Data objects
 *
 * @package common
 */

/**
 * Generic Data Object class
 *
 * Contains default __construct, __get and __set functions.
 * For complex data retrieval operations, override __get to check for
 * "complex" values to do the fancy stuff and call parent::__get for simple
 * operations
 *
 * @package common
 */
abstract class DataObject implements Iterator {
	protected $properties;
	protected $propertyNames;
	protected $indirectProperties = array();
	protected $customProperties = array();
	protected $cursor = 0;	// for Iterator

	/**
	 * Default Data Object constructor
	 *
	 * Initializes the Data Object in a generic manner. If a parameter is passed,
	 * it is expected to be an array with keys corresponding to the object's
	 * property names or an object with properties whose names correspond to the
	 * property names of the data object. Properties for which a value is provided
	 * will be set. Keys in $properties that are not recognized are ignored.
	 *
	 * @param $properties An associative array containing inital property values
	 * @throws Exception when the extending class has not defined $propertyNames
	 */
	public function __construct($properties = NULL) {
		if (!isset($this->propertyNames)) {
			throw new Exception("DataObject not properly defined");
		}

		/* initialize empty object */
		$this->properties = array();
		if ($properties !== NULL) {
			$this->init($properties);
		}
	}
	
	/**
	 * Inits an already constructed object from an associative array
	 *
	 * This function is valuable when you want to quickly set certain fields
	 * in an already filled in object
	 */
	public function init($properties) {
		/* initialize from array */
		foreach ($this->propertyNames as $key) {
			if (isset($properties[$key])) {
				$this->__set($key, $properties[$key]);
			}
		}
	}

	/**
	 * Default magic __get method
	 *
	 * Gets a property (if it is defined in <var>$propertyNames</var>. Will
	 * generate an E_USER_NOTICE if not.
	 */
	public function __get($property) {
		if (in_array($property, $this->propertyNames)) {
			if (isset($this->properties[$property])) {
				/* return stored/cached values directly */
				return $this->properties[$property];
			} else {
				if (!empty($this->indirectProperties[$property])) {
					/* attempt lookup */
					$type = $this->indirectProperties[$property];
					$method = "get" . $type . "ByID";
					/* cache result internally */
					$this->properties[$property] = DML::getInstance()->$method($this->properties[$property . "ID"]);
					return $this->properties[$property];
				}
				return NULL;
			}
		} else {
			user_error("Unknown property: " . $property, E_USER_NOTICE);
			return NULL;
		}
	}

	/**
	 * Default magic __set method
	 *
	 * Sets a (named) property to a provided value. Generates an E_USER_NOTICE
	 * when the property is not defined in <var>$propertyNames</var>
	 */
	public function __set($property, $value) {
		if (in_array($property, $this->propertyNames)) {
			return $this->properties[$property] = $value;
		} else {
			user_error("Unknown property: " . $property . ", ignored", E_USER_NOTICE);
		}
	}

	/**
	 * Default magic __unset method
	 *
	 * Because properties are not declared conventionally but through the
	 * <var>$propertyNames</var> property, an altered __unset method is required.
	 */
	public function __unset($property) {
		if (in_array($property, $this->propertyNames) && isset($this->properties[$property])) {
			unset($this->properties[$property]);
		}
	}

	/**
	 * Determine whether a property is indirect or not
	 */
	public function isIndirectProperty($propertyName) {
		return isset($this->indirectProperties[$propertyName]);
	}

	/**
	 * Determine whether a property is custom or not
	 */
	public function isCustomProperty($propertyName) {
		return in_array($propertyName, $this->customProperties);
	}

	/**
	 * __isset method
	 */
	public function __isset($property) {
		//$this->__get($property);	// force retrieval of "delayed" properties
		return isset($this->properties[$property]);
	}

	/**
	 * Iterator::rewind
	 */
	public function rewind() {
		$this->cursor = 0;
	}

	/**
	 * Iterator::current
	 */
	public function current() {
		return $this->__get($this->propertyNames[$this->cursor]);
	}

	/**
	 * Iterator::key
	 */
	public function key() {
		return($this->propertyNames[$this->cursor]);
	}

	/**
	 * Iterator::next
	 */
	public function next() {
		$this->cursor++;
	}

	/**
	 * Iterator::valid
	 */
	public function valid() {
		return isset($this->propertyNames[$this->cursor]);
	}
}
